拿到它,先看它是什麼?拿它來做什麼? 再決定。
在複製陣列之前,先理解了 JavaScript 會依資料型別 Primitive type(基本型別)和Non-primitive type(非基本型別)而有不同的運作方式,我們就可以進一步的來了解,我們常聽到的深拷貝(DeepCopy)和淺拷貝(shallow) 是什麼、在什麼情境下要選擇哪一種複製方法、如何實作出這些深拷貝和淺拷貝。
在生活中我們最常做的拷貝就是影印,假如我有一份筆記,朋友跟我借去拷貝,當他拿著我的筆記,到影印機前,按下拷貝鍵的那一刻,複製的筆記從機器跑出來,這時「拷貝」這件事就算完成,「我的筆記」和「他影印的筆記」是各自獨立的筆記,之後他要在這份影印的筆記上塗鴉也好、修改也好,也不會影響我的筆記。
但是在 JavaScript 裡,當我們新建立一個變數a
,賦予這個變數一個值,然後再建立一個新變數b
,接著以=
指定運算子來把a
指定給b
,這樣就算拷貝嗎?雖然我們a
、b
叫出來看都是ˋ42ˋ,但是這真的是拷貝嗎。?是或不是?
let a = 42;
let b = a;
a; // 42
b; // 42
事實上,這得要看我們要複製什麼東西,才能知道我們是否可以「輕易」的複製,且「真正」的把複製和被複製的徹底分離。
更確切的說,如果我們要複製的變數值型態,是屬於Primitive data types(基本資料型別),也就是以下的資料型態:
42
"Hi"
true
那麼我們就可以放心的以上述的方式複製。
還記得前一篇提到的 Call by value(呼叫變數的值)嗎?這些資料型別在 JavaScript 裡是屬於 Immutable (不可變)的基本型別,以值的方式複製可以得到真正的、獨立的複製。
但是,如果今天要被複製的資料型態是 Non Primitive data types(非基本資料型別),也就是 Object (物件型別),那就無法完全複製。例如:
[1, 2, 3, 4, 5]
{ name : "Tsuifei"}
因為陣列和物件是屬於 Call by reference(呼叫變數的記憶體位址),也就是說當我們複製這類型的資料,只是複製了這個變數的記憶體位置,所以當我們呼叫複製和被複製的變數時,都會指向同一個記憶體位置,當然,裡面的值也是同一個值,改任何一個,都會動到兩個變數的值。
我們再來複習一下前一篇的範例:
// by reference(參考值)
let person = {
name: "Tracy",
city: "Tainan"
}
let person2 = person;
person2.name = "Ayda"
person; // name: "Ayda"
person2; // name: "Ayda"
我們可以看到,在我們修改從person
複製出來的person2
時,person
也被修改了。
終於,我們要進入 深眠和淺眠 這個正題了。
不知大家有沒發現,在討論這個深淺拷貝的範例時,清一色都是用物件來示範?原來,「深拷貝」和「淺拷貝」是針對物件的資料型別複製時,所產生的現象而來的啊!
但是要如何在 JavaScript 中區分深拷貝和淺拷貝?何時該用「深拷貝」或「淺拷貝」,用最簡單的方式是取決於我們想要複製的資料[元素]
是什麼型別。 結束。
完全的複製 Array 而不受原陣列影響,即使修改複製過來的物件裡的值,也不會改變複製來源,這個物件裡面的「元素」可以是任何一種資料型態,反著說,就是這個物件裡面的元素,不能是物件。如果遇到這樣的資料,就可以用淺拷貝的方法複製。
有哪幾種方法可以做淺拷貝?最常被拿來用的是 JavaScript 內建的陣列方法slice()
。 它的詳細解說會在後幾章才會介紹到。 slice()
通常拿來做從陣列中切取我們需要的元素出來,在這裡我們使用slice(0)
表示我們要從頭到尾都切下來。 切切切
在下面第一個範例,「淺拷貝」是可行的,因為arr1
陣列裡的元素是基本資料型別Number
let arr1 = [1,2,3];
let arr2 = arr1.slice(0);
arr2[0] = 42;
arr1; // [1, 2, 3]
arr2; // [42, 2, 3]
但是以下這個範例,arr1
陣列裡的「元素」是「物件型別」的陣列,淺拷貝對於原物件裡面的元素值是物件型態就是不行! 噠美噠美
// 淺拷貝 []
let arr1 = [[1,2,3],[4,5,6]];
let arr2 = arr1.slice(0);
arr2[0][0] = 42;
arr1;
// 0: (3) [42, 2, 3]
// 1: (3) [4, 5, 6]
arr2;
// 0: (3) [42, 2, 3]
// 1: (3) [4, 5, 6]
網路上能找到的大多是淺拷貝的例子,雖然淺 可別因此就鄙視它,只要確認要複製的來源物件,裡面的元素不是物件型別,還是非常好用的。
礙於篇幅,這裡只介紹一種淺拷貝的方式,有興趣的朋友可查找網路上其他的方法,例如用解構式、迴圈或使用map()
都可達到淺拷貝的效果。
何時使用深拷貝?當我們想要完全複製一份「物件」裡面的元素也是「物件」,就可以使用深拷貝,這種情境就是我們在本文開頭所說的,用影印機複製筆記ㄧ樣,複製完就是兩個獨立的個體了。
做深拷貝的方法並不多,大部分都是靠外來的函式庫來撐腰,例如 lodash 和 jQuery 的第三方主流函式庫,如果使用原生的 JavaScript 來做深拷貝,似乎只能使用JSON.stringify()
和JSON.parse()
的交互作用,達到深拷貝的效果。
來看一下 MDN 對這兩個函式的解釋: JSON.stringify()| MDN | JSON.parse() | MDN
JSON.stringify()
方法是將一個 JavaScript 的值(物件或陣列)轉換為一個JSON
的字串。
let arr = [[1,2,3],[4,5,6]];
arr = JSON.stringify(arr); // "[[1,2,3],[4,5,6]]"
JSON.parse()
方法用來解析JSON
的字串,構造由字串描述的JavaScript值或物件。
這時的arr
已經變成JSON的字串格式:"[[1,2,3],[4,5,6]]"
。
接下來再轉回陣列的型態:
arr = JSON.parse(arr); // [[1,2,3],[4,5,6]]
把原本的物件值轉成字串,然後再轉回來物件的型態,兩個函式手牽手處理下來,就等於複製了一份arr1
到arr2
。
function jsonDeepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
let arr1 = [[1,2,3],[4,5,6]];
let arr2 = jsonDeepClone(arr1);
arr2[0][0]=42;
arr1;
// 0: (3) [1, 2, 3]
// 1: (3) [4, 5, 6]
arr2;
// 0: (3) [42, 2, 3]
// 1: (3) [4, 5, 6]
這樣的一個拷貝過程與結果就是深拷貝(DeepCopy)了。
優比~週末了!但是別人過週末,我們還是要過鋼鐵,明天繼續囉~
如有需要改進的地方,拜託懇求請告知,我會盡量快速度修改,感謝您~